Skip to content

feat(plugin): add host based plugin configuration, pipeline and lifecycle hooks - #35

Merged
rian-be merged 9 commits into
developmentfrom
Host-Lifecycle
Sep 12, 2026
Merged

rian-be merged 9 commits into
developmentfrom
Host-Lifecycle

Conversation

@rian-be

@rian-be rian-be commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR extends the plugin contract with the host builder and a scoped configuration context, adds explicit application pipeline and endpoint hooks, and bridges plugin lifecycle hooks and plugin-owned hosted services to the standard .NET host lifecycle. The behavior is verified against the real host entry point with integration tests and documented in three new ADRs.

Configuration Context and Host Builder

  • adds ConfigureServices overloads for IHostApplicationBuilder and for IServiceCollection with AuthKitPluginContext
  • selects the most specific configuration overload implemented by each plugin
  • provides plugin-scoped configuration sections keyed by plugin id, falling back to plugin name

Endpoints and Application Pipeline Hooks

  • adds ConfigureApplication and pipeline position aware ConfigurePipeline hooks with PluginPipelinePosition
  • maps plugin endpoints onto the application route builder through MapEndpoints
  • keeps the legacy MiddlewareType entry point as a compatibility fallback for plugins that do not opt into the new hooks
  • rejects unsupported declared pipeline positions before any hook runs

Lifecycle Hooks and Hosted Services

  • adds PluginLifecycleHostedService bridging OnStarting/OnStarted/OnStopping to IHostApplicationLifetime
  • registers plugin owned hosted services exactly once, ordered by plugin id
  • deduplicates hosted service instances by reference and rejects duplicate registrations

Integration Tests

  • adds tests/Host.IntegrationTests driving the real Program through WebApplicationFactory
  • verifies the real DevTokens plugin is discovered, passes contract validation, and serves requests
  • covers full lifecycle ordering, cancellation token propagation, and duplicate/normal hosted service registration against a real generic host
  • runs without an external PostgreSQL via an in-memory keystore repository and test only Wolverine durability mode

Documentation

  • adds ADR-022 for the plugin configuration context and host builder
  • adds ADR-023 for application pipeline and endpoint hooks
  • adds ADR-024 for plugin lifecycle hooks and hosted services

Validation

  • 99/99 tests pass across AuthKit.Plugins.Abstractions.Tests, AuthKit.Host.Tests, and AuthKit.Host.IntegrationTests
  • dotnet build completes with zero errors across the solution
  • git diff --check passes

Result

AuthKit plugins can now configure services through the host builder and scoped context, integrate into the application pipeline at explicit positions, and participate in the standard .NET host lifecycle with their own hosted services all verified against the real host entry point.

Closes #8
Closes #9
Closes #10

Comment thread src/Host/Plugins/PluginApplicationConfiguration.cs Dismissed
Comment thread src/Host/Plugins/PluginApplicationConfiguration.cs Dismissed
Comment thread src/Host/Plugins/PluginApplicationConfiguration.cs Dismissed
Comment on lines +122 to +132
foreach (var loadedPlugin in Ordered(plugins))
{
var plugin = loadedPlugin.Plugin;
if (plugin.MiddlewareType is null
|| HasImplementation(plugin, nameof(IAuthKitPlugin.ConfigureApplication), typeof(IApplicationBuilder))
|| HasImplementation(plugin, nameof(IAuthKitPlugin.ConfigurePipeline),
typeof(IApplicationBuilder), typeof(PluginPipelinePosition)))
continue;

application.UseMiddleware(plugin.MiddlewareType);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit foreach is deliberate here: the loop body uses plugin more than once (and ConfigurePipeline also continues on a position filter), so collapsing to .Select(...) would not improve readability. No functional impact -- dismissing.

Comment on lines +152 to +162
foreach (var loadedPlugin in plugins)
{
var plugin = loadedPlugin.Plugin;
if (HasImplementation(plugin, nameof(IAuthKitPlugin.ConfigurePipeline),
typeof(IApplicationBuilder), typeof(PluginPipelinePosition))
&& !Enum.IsDefined(plugin.PipelinePosition))
{
throw new InvalidOperationException(
$"Plugin '{plugin.Id}' declares unsupported pipeline position '{plugin.PipelinePosition}'.");
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit foreach is deliberate here: the loop body uses plugin more than once (and ConfigurePipeline also continues on a position filter), so collapsing to .Select(...) would not improve readability. No functional impact -- dismissing.

Comment on lines +51 to +56
foreach (var loadedPlugin in _plugins)
{
var plugin = loadedPlugin.Plugin;
logger.LogDebug("Starting plugin '{PluginId}'.", plugin.Id);
Invoke(plugin, "OnStarting", () => plugin.OnStartingAsync(cancellationToken));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit foreach is deliberate here: the loop body uses plugin more than once (and ConfigurePipeline also continues on a position filter), so collapsing to .Select(...) would not improve readability. No functional impact -- dismissing.

Comment on lines +60 to +65
foreach (var loadedPlugin in _plugins)
{
var plugin = loadedPlugin.Plugin;
logger.LogDebug("Plugin '{PluginId}' started.", plugin.Id);
Invoke(plugin, "OnStarted", () => plugin.OnStartedAsync(lifetime.ApplicationStopping));
}

@rian-be rian-be Sep 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit foreach is deliberate here: the loop body uses plugin more than once (and ConfigurePipeline also continues on a position filter), so collapsing to .Select(...) would not improve readability. No functional impact dismissing.

Comment on lines +70 to +75
foreach (var loadedPlugin in _plugins.Reverse())
{
var plugin = loadedPlugin.Plugin;
logger.LogDebug("Stopping plugin '{PluginId}'.", plugin.Id);
Invoke(plugin, "OnStopping", () => plugin.OnStoppingAsync(lifetime.ApplicationStopping));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explicit foreach is deliberate here: the loop body uses plugin more than once (and ConfigurePipeline also continues on a position filter), so collapsing to .Select(...) would not improve readability. No functional impact -- dismissing.

Comment on lines +108 to +112
catch (Exception ex)
{
throw new InvalidOperationException(
$"Plugin '{plugin.Id}' failed during {stage}.", ex);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional: the catch wraps any failure from a plugin-run hook with plugin and stage context and rethrows, so a broken plugin fails loudly during host startup or shutdown. Narrowing the exception type would defeat that purpose. Accepting as designed.

Comment thread tests/Host.IntegrationTests/AuthKitWebApplicationFactory.cs Fixed
@rian-be rian-be added enhancement New feature or request documentation Improvements or additions to documentation contract Changes the plugin contract additive Additive, non-breaking change area/host Host-side runtime (DI, OpenAPI, health exec) area/abstractions AuthKit.Plugins.Abstractions contract labels Sep 12, 2026
@rian-be
rian-be requested review from RX-J and sean6224 September 12, 2026 17:40
@rian-be rian-be changed the title feat(plugin): add host-based plugin configuration, pipeline, and lifecycle hooks feat(plugin): add host based plugin configuration, pipeline and lifecycle hooks Sep 12, 2026
@rian-be
rian-be removed the request for review from sean6224 September 12, 2026 19:16
@rian-be
rian-be merged commit ad005d0 into development Sep 12, 2026
7 of 9 checks passed
@rian-be
rian-be deleted the Host-Lifecycle branch September 12, 2026 19:17
@rian-be rian-be self-assigned this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

additive Additive, non-breaking change area/abstractions AuthKit.Plugins.Abstractions contract area/host Host-side runtime (DI, OpenAPI, health exec) contract Changes the plugin contract documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

3 participants